package org.manon.json.asm.ser;

import static org.manon.json.asm.ASMUtil.TYPE_FILTER;
import static org.manon.json.asm.ASMUtil.TYPE_OUTPUTSTREAM;
import static org.manon.json.asm.ASMUtil.TYPE_STRING;
import static org.manon.json.asm.ASMUtil.TYPE_VOID;

import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.Iterator;

import org.manon.json.JSONException;
import org.manon.json.analyse.TreeNode;
import org.manon.json.asm.ASMBaseGenerator;
import org.manon.json.asm.ASMContextSerializer;
import org.manon.json.asm.ASMGenerator4Serializer;
import org.manon.json.asm.ASMUtil;
import org.manon.json.meta.Entity;
import org.manon.json.meta.MetaProperty;
import org.manon.json.meta.MetaPropertyAssociation;
import org.manon.json.meta.MetaPropertyCollection;
import org.manon.json.meta.MetaPropertyDiscriminator;
import org.manon.json.meta.MetaPropertyGeneric;
import org.manon.json.meta.MetaPropertyMap;
import org.manon.json.meta.MetaPropertySingleValue;
import org.manon.json.meta.MetaPropertyVisitor;
import org.manon.json.util.ClassUtils;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

/**
 * @author <a href="mailto:jacques.militello@gmail.com">Jacques Militello</a>
 */
final class CreateFieldVisitor implements MetaPropertyVisitor{

	private final MethodVisitor _staticInitBlock;
	private final ASMBaseGenerator _generator;
	private final ASMContextSerializer _context; 
	
	
	public CreateFieldVisitor(ASMBaseGenerator generator,ASMContextSerializer context, MethodVisitor initBlock) {
		_generator = generator;
		_context = context;
		_staticInitBlock = initBlock;
	}

	/**
	 * @see org.manon.json.meta.MetaPropertyVisitor#visit(org.manon.json.meta.MetaPropertyCollection)
	 */
	public void visit(MetaPropertyCollection property) {
		createStaticField(_staticInitBlock,_context,property);
		createInstance(_generator, _context, property.getCollectionItemClass(), property);
	}

	/**
	 * @see org.manon.json.meta.MetaPropertyVisitor#visit(org.manon.json.meta.MetaPropertyAssociation)
	 */
	public void visit(MetaPropertyAssociation property) {
		createStaticField(_staticInitBlock,_context,property);
		createInstanceField(_generator, _context, property.getReturnType(), property.getName());
	}

	/**
	 * @see org.manon.json.meta.MetaPropertyVisitor#visit(org.manon.json.meta.MetaPropertyMap)
	 */
	public void visit(MetaPropertyMap property) {
		createStaticField(_staticInitBlock,_context,property);
		
		if (!ASMUtil.isSingleValue(property.getKeyItem())) {
			createInstanceField(_generator, _context, property.getKeyItem(), property.getKeyItem().getSimpleName());
		}

		if (!ASMUtil.isSingleValue(property.getValueItem())) {
			createInstanceField(_generator, _context, property.getValueItem(), property.getValueItem().getSimpleName());
		}
		
	}

	
	/**
	 * @see org.manon.json.meta.MetaPropertyVisitor#visit(org.manon.json.meta.MetaPropertyGeneric)
	 */
	public void visit(MetaPropertyGeneric property) {
		createStaticField(_staticInitBlock,_context,property);
	}
	/**
	 * @see org.manon.json.meta.MetaPropertyVisitor#visit(org.manon.json.meta.MetaPropertyDiscriminator)
	 */
	public void visit(MetaPropertyDiscriminator property) {
		createStaticField(_staticInitBlock,_context,property);
	}
	/**
	 * @see org.manon.json.meta.MetaPropertyVisitor#visit(org.manon.json.meta.MetaPropertySingleValue)
	 */
	public void visit(MetaPropertySingleValue property) {
		createStaticField(_staticInitBlock,_context,property);
	}
	
	private void createStaticField(MethodVisitor mv,ASMContextSerializer context, MetaProperty property)  {
		// public static final String FIELD_<property.name> = property.name 
		FieldVisitor visitor = context.getClassWriter().visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "FIELD_BYTE_" + property.getName().toUpperCase() , Type.getDescriptor(byte[].class), null, null);
		visitor.visitEnd();
		
		mv.visitLdcInsn(property.getLabel());
		mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "getBytes", Type.getMethodDescriptor(Type.getType(byte[].class)));
		mv.visitFieldInsn(Opcodes.PUTSTATIC, context.getClassName(), "FIELD_BYTE_" + property.getName().toUpperCase(), Type.getDescriptor(byte[].class));
		
		if (property instanceof MetaPropertyDiscriminator) {
			addFieldDiscriminator(mv,context,(MetaPropertyDiscriminator)property);
		}
	}
	
	private void addFieldDiscriminator(MethodVisitor mv,ASMContextSerializer context,MetaPropertyDiscriminator property) {

		switch (property.getDiscriminatorType()) {
		case CLASS_NAME:
			FieldVisitor visitor = context.getClassWriter().visitField(ASMUtil.publicStaticFinal(), ASMGenerator4Serializer.FIELD_DISCRIMINATOR_PREFIX + property.getName().toUpperCase() , Type.getDescriptor(byte[].class), null, null);
			visitor.visitEnd();
			mv.visitLdcInsn(context.getEntity().getId());	
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "getBytes", Type.getMethodDescriptor(Type.getType(byte[].class)));
			mv.visitFieldInsn(Opcodes.PUTSTATIC, context.getClassName(),ASMGenerator4Serializer.FIELD_DISCRIMINATOR_PREFIX + property.getName().toUpperCase(), Type.getDescriptor(byte[].class));
			break;
		case PROPERTY:
			// nothing to do.
			break;
		}
		
		
		// create method getDiscriminator
		switch (property.getDiscriminatorType()) {
		case CLASS_NAME: {
			MethodVisitor methodDiscriminator = context.getClassWriter().visitMethod(Opcodes.ACC_PUBLIC,"getDiscriminator",Type.getMethodDescriptor(Type.getType(byte[].class)),null,null);
			methodDiscriminator.visitCode();
			methodDiscriminator.visitFieldInsn(Opcodes.GETSTATIC, context.getClassName(), ASMGenerator4Serializer.FIELD_DISCRIMINATOR_PREFIX + property.getName().toUpperCase(), Type.getDescriptor(byte[].class));
			methodDiscriminator.visitInsn(Opcodes.ARETURN);
			methodDiscriminator.visitMaxs(1, 1);
			methodDiscriminator.visitEnd();
			break;
		}
		case PROPERTY: {
			MethodVisitor methodDiscriminator = context.getClassWriter().visitMethod(Opcodes.ACC_PUBLIC,"getDiscriminator",Type.getMethodDescriptor(Type.getType(byte[].class),Type.getType(context.getEntity().getEntityClass())),null,null);
			methodDiscriminator.visitVarInsn(Opcodes.ALOAD, 1);
			methodDiscriminator.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(context.getEntity().getEntityClass()), ClassUtils.getGetter(property.getProperty()), Type.getMethodDescriptor(TYPE_STRING));
			methodDiscriminator.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "getBytes", Type.getMethodDescriptor(Type.getType(byte[].class)));
			methodDiscriminator.visitInsn(Opcodes.ARETURN);
			methodDiscriminator.visitMaxs(1, 1);
			methodDiscriminator.visitEnd();
			break;
		}
		}
		
	}
	
	private static void createInstanceField(ASMBaseGenerator generator,ASMContextSerializer context,Class<?> item, String propertyName) {
		
//		if (item.isInterface()) {
//			context.appendToConstructorInterface(property, item);
//			return;
//		}
		
		final String path[] = ClassUtils.path(item);
		
		if (generator.getRegistry().searchPath(path).isLeaf()) {
			
			final String fieldName = item.getSimpleName().toLowerCase()+ASMGenerator4Serializer.FIELD_SUFFIX;

			if (context.getFields().contains(fieldName)) {
				return;
			}
			
			final String descriptor;
			Entity entity = generator.findEntityClass(item);
			if (entity.getCustomSerializer() == null) {
				descriptor = ASMUtil.getDescriptor(entity.getInternalClassNameForSerializer());
			} else {
				descriptor = Type.getDescriptor(entity.getCustomSerializer());
			}
			
			context.getClassWriter().visitField(Opcodes.ACC_FINAL + Opcodes.ACC_PRIVATE, fieldName, descriptor, null, null);
			context.appendToConstructor(item);
			context.getFields().add(fieldName);
			return;
		} 
			
		if (Object.class.equals(item)) {
			// use serializer provider.
			throw new JSONException("Object Item is unsupported");
		} else {
		
			final MethodVisitor methodCollection = context.getClassWriter().visitMethod(Opcodes.ACC_PUBLIC, propertyName, 
					Type.getMethodDescriptor(TYPE_VOID, Type.getType(item) , TYPE_OUTPUTSTREAM, TYPE_FILTER), null, new String[]{Type.getInternalName(IOException.class)});
			methodCollection.visitCode();
			createFields(context,methodCollection,generator.getRegistry().searchPath(path));
			methodCollection.visitInsn(Opcodes.RETURN);
			methodCollection.visitMaxs(1, 1);
			methodCollection.visitEnd();
		}
	}
	
	public static void createInstance(ASMBaseGenerator generator,ASMContextSerializer context,Class<?> item, MetaPropertyCollection property) {
		
		if (item.isInterface()) {
			//context.appendToConstructorInterface(property, item);
			return;
		}
		
		final String path[] = ClassUtils.path(item);
		
		if (generator.getRegistry().searchPath(path).isLeaf()) {
			
			final String fieldName = item.getSimpleName().toLowerCase()+ASMGenerator4Serializer.FIELD_SUFFIX;
			
			if (context.getFields().contains(fieldName)) {
				return;
			}
			
			final String descriptor;
			
			if (property.getCollectionItemSerializer() != null) {
				descriptor = Type.getDescriptor(property.getCollectionItemSerializer());
			} else {
				Entity entity = generator.findEntityClass(item);
				descriptor = ASMUtil.getDescriptor(entity.getInternalClassNameForSerializer());
			}
			
			context.appendToConstructor(property, item);
			
			context.getClassWriter().visitField(Opcodes.ACC_FINAL + Opcodes.ACC_PRIVATE, fieldName, descriptor, null, null);
			context.getFields().add(fieldName);
			return;
		} 
			
		if (Object.class.equals(item)) {
			// use serializer provider.
			throw new JSONException("Object Item is unsupported");
		} else {
		
			final MethodVisitor methodCollection = context.getClassWriter().visitMethod(Opcodes.ACC_PUBLIC, property.getName(), 
					Type.getMethodDescriptor(TYPE_VOID, Type.getType(item) , TYPE_OUTPUTSTREAM, TYPE_FILTER), null, new String[]{Type.getInternalName(IOException.class)});
			methodCollection.visitCode();
			createFields(context,methodCollection,generator.getRegistry().searchPath(path));
			methodCollection.visitInsn(Opcodes.RETURN);
			methodCollection.visitMaxs(1, 1);
			methodCollection.visitEnd();
		}
	}
	
	private static void createFields(ASMContextSerializer context,MethodVisitor methodCollection,TreeNode<Entity, String> nodeParent) {
		TreeNode<Entity, String> node;
		final Iterator<TreeNode<Entity, String>> iterator = nodeParent.iterator();
		
		while (iterator.hasNext()) {
			node = iterator.next();
			if (node.isLeaf()) {
				addX(context,methodCollection,node);
			} else {
				createFields(context,methodCollection,node);
			}
		}
		
		if (!Modifier.isAbstract( nodeParent.getObject().getEntityClass().getModifiers())) {
			addX(context,methodCollection,nodeParent);
		}
		
	}
	
	private static void addX(ASMContextSerializer context,MethodVisitor methodCollection,TreeNode<Entity, String> node) {
		final Entity entity = node.getObject();
		context.getClassWriter().visitField(Opcodes.ACC_FINAL + Opcodes.ACC_PUBLIC, node.getId().toLowerCase()+ASMGenerator4Serializer.FIELD_SUFFIX, ASMUtil.getDescriptorForSerializer(entity), null, null);
		context.appendToConstructor(entity);
		Label returnLabel = new Label();
		methodCollection.visitVarInsn(Opcodes.ALOAD, 1);
		methodCollection.visitTypeInsn(Opcodes.INSTANCEOF,Type.getInternalName(entity.getEntityClass()));
		methodCollection.visitJumpInsn(Opcodes.IFEQ, returnLabel);
		methodCollection.visitVarInsn(Opcodes.ALOAD, 0);
		methodCollection.visitFieldInsn(Opcodes.GETFIELD, context.getClassName(), node.getId().toLowerCase()+ASMGenerator4Serializer.FIELD_SUFFIX,ASMUtil.getDescriptorForSerializer(entity));
		methodCollection.visitVarInsn(Opcodes.ALOAD, 2);
		methodCollection.visitVarInsn(Opcodes.ALOAD, 1);
		methodCollection.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(node.getObject().getEntityClass()));
		methodCollection.visitVarInsn(Opcodes.ALOAD, 3);
		methodCollection.visitMethodInsn(Opcodes.INVOKEVIRTUAL,entity.getInternalClassNameForSerializer(),"serialize",
				Type.getMethodDescriptor(TYPE_VOID, TYPE_OUTPUTSTREAM,Type.getType(entity.getEntityClass()),TYPE_FILTER));
		methodCollection.visitInsn(Opcodes.RETURN);
		methodCollection.visitLabel(returnLabel);
	}
}